2013-02-19 20 views
5

Mi piace avere a portata di mano un calcolatore da riga di comando. I requisiti sono:Calcolatrice da riga di comando minima da digitare - tcsh vs bash

  • Supporta tutti gli operatori aritmetici di base: +, -, /, *,^per l'esponenziazione, più le parentesi per il raggruppamento.
  • Richiede una digitazione minima, non voglio dover chiamare un programma per interagire con esso, quindi chiedergli di uscire.
  • Idealmente solo un carattere e uno spazio oltre all'espressione stessa devono essere inseriti nella riga di comando.
  • Dovrebbe sapere come ignorare virgole e dollaro (o altri simboli di valuta) nei numeri da permettermi di copiare/incollare dal web senza preoccuparsi di dover pulire ogni numero prima di incollarlo nella calcolatrice
  • Be white-space tolleranti, la presenza o la mancanza di spazi non dovrebbero causare errori
  • non c'è bisogno di citare nulla nell'espressione per proteggerlo dalla shell - ancora una volta a beneficio di minimo di battitura

dal tcsh supporta alias argomenti posizionali, e poiché l'espansione degli alias precede tutte le altre espansioni A parte l'espansione della storia, è stato semplice implementare qualcosa vicino al mio ideale in tcsh.

ho usato questo:

alias C 'echo '\''\!*'\'' |tr -d '\'',\042-\047'\'' |bc -l' 

ora posso fare cose come le seguenti operazioni con tipizzazione minima:

# the basic stuff: 
tcsh> C 1+2 
3 

# dollar signs, multiplication, exponentiation: 
tcsh> C $8 * 1.07^10 
15.73721085831652257992 

# parentheses, mixed spacing, zero power: 
tcsh> C (2+5)/8 * 2^0 
.87500000000000000000 

# commas in numbers, no problem here either: 
tcsh> C 1,250.21 * 1.5 
1875.315 

Come si può vedere non c'è bisogno di citare qualsiasi cosa per fare tutti questi lavori.

Ora arriva il problema. Cercando di fare lo stesso in bash, in cui parametri alias non sono supportate le forze me per implementare la calcolatrice in funzione shell e passare i parametri utilizzando "$ @"

function C() { echo "[email protected]" | tr -d ', \042-\047' | bc -l; } 

Questo rompe in vari modi, ad esempio:

# works: 
bash$ C 1+2 
3 

# works: 
bash$ C 1*2 
2 

# Spaces around '*' lead to file expansion with everything falling apart: 
bash$ C 1 * 2 
(standard_in) 1: syntax error 
(standard_in) 1: illegal character: P 
(standard_in) 1: illegal character: S 
(standard_in) 1: syntax error 
... 

# Non-leading parentheses seem to work: 
bash$ C 2*(2+1) 
6 

# but leading-parentheses don't: 
bash$ C (2+1)*2 
bash: syntax error near unexpected token `2+1' 

Ovviamente, l'aggiunta di virgolette intorno all'espressione risolve questi problemi, ma è contro i requisiti originali.

Capisco perché le cose si rompono in bash. Non sto cercando spiegazioni. Piuttosto, sto cercando una soluzione che non richieda manualmente la citazione degli argomenti. La mia domanda per bash wizard è c'è un modo per fare bash supportare il pratico alias minimo calcolatrice di battitura. Non richiede quoting, come fa tcsh? È impossibile? Grazie!

+0

I primi due requisiti sono in conflitto. Se si desidera una sequenza di tasti minima, si desidera la lucidatura inversa, in modo da non richiedere nemmeno le parentesi per il raggruppamento. –

+0

Abbastanza giusto.Intendevo minimamente mantenendo la notazione umana naturale :) – arielf

+0

Ripensare a questo: il reverse-polish richiederebbe un operatore 'push/enter' tra due numeri, quindi infisso 1 + 2 (3 caratteri) richiede in realtà meno input di tipizzazione rispetto al reverse-polish: 1 2+ (4 caratteri) - solo come esempio. – arielf

risposta

2

Almeno prevenire l'espansione di * è possibile utilizzando 'set -f' (in seguito qualcuno blog post:

alias C='set -f -B; Cf ' 
function Cf() { echo "[email protected]" | tr -d ', \042-\047' | bc -l; set +f; }; 

Girando fuori in l'alias, prima del calcolo, e di nuovo in seguito

$ C 2 * 3 
6 

Ho scaricato i sorgenti di bash e ho osservato molto attentamente: sembra che l'errore di parentesi si verifichi direttamente durante l'analisi della riga di comando, prima che venga eseguito qualsiasi comando o r alias è espanso. E senza alcuna bandiera per spegnerlo. Quindi sarebbe impossibile farlo da uno script bash.

Questo significa che è ora di portare le armi pesanti. Prima di analizzare la riga di comando viene letta da stdin usando readline. Pertanto, se intercettiamo la chiamata a readline, possiamo fare tutto ciò che vogliamo con la riga di comando.

Purtroppo bash è collegato staticamente a readline, quindi la chiamata non può essere intercettata direttamente. Ma almeno readline è un simbolo globale, quindi possiamo ottenere l'indirizzo della funzione usando dlsym, e con quell'indirizzo possiamo inserire istruzioni arbitrarie in readline.

modificando readline è direttamente potare ad errori, se readline è cambiato tra le diverse versioni di bash, quindi abbiamo modificare la readline funzione di chiamata, che porta alla seguente piano:

  1. Individuare readline con dlsym
  2. Sostituire readline con la nostra funzione che utilizza lo stack corrente per individuare la funzione che chiama readline (yy_readline_get) durante la prima chiamata e ripristina la readline originale
  3. Modifica yy_readline_get per chiamare la nostra funzione wrapper
  4. W ntro la funzione wrapper: Sostituire le parentesi con simboli non problematici, se l'ingresso inizia con "C"

Scritto in C per amd64, otteniamo:

#include <string.h> 
#include <stdio.h> 
#include <stdint.h> 
#include <stdlib.h> 
#ifndef __USE_GNU 
#define __USE_GNU 
#endif 
#ifndef __USE_MISC 
#define __USE_MISC 
#endif 
#include <dlfcn.h> 
#include <unistd.h> 
#include <sys/mman.h> 
#include <errno.h> 

//-----------Assembler helpers---------- 

#if (defined(x86_64) || defined(__x86_64__)) 

    //assembler instructions to read rdp, which we need to read the stack 
#define MOV_EBP_OUT "mov %%rbp, %0" 
    //size of a call instruction 
#define RELATIVE_CALL_INSTRUCTION_SIZE 5 

#define IS64BIT (1) 

    /* 
     To replace a function with a new one, we use the push-ret trick, pushing the destination address on the stack and let ret jump "back" to it 
     This has the advantage that we can set an additional return address in the same way, if the jump goes to a function 

    This struct corresponds to the following assembler fragment:   
    68  ???? push     <low_dword (address)> 
    C7442404 ???? mov DWORD PTR [rsp+4], <high_dword (address)) 
    C3    ret 
    */ 
typedef struct __attribute__((__packed__)) LongJump { 
    char push; unsigned int destinationLow; 
    unsigned int mov_dword_ptr_rsp4; unsigned int destinationHigh; 
    char ret; 
// char nopFiller[16]; 
} LongJump; 

void makeLongJump(void* destination, LongJump* res) { 
    res->push = 0x68; 
    res->destinationLow = (uintptr_t)destination & 0xFFFFFFFF; 
    res->mov_dword_ptr_rsp4 = 0x042444C7; 
    res->destinationHigh = ((uintptr_t)(destination) >> 32) & 0xFFFFFFFF; 
    res->ret = 0xC3; 
} 

//Macros to save and restore the rdi register, which is used to pass an address to readline (standard amd64 calling convention) 
typedef unsigned long SavedParameter; 
#define SAVE_PARAMETERS SavedParameter savedParameters; __asm__("mov %%rdi, %0": "=r"(savedParameters)); 
#define RESTORE_PARAMETERS __asm__("mov %0, %%rdi": : "r"(savedParameters)); 

#else 
#error only implmented for amd64... 
#endif 

//Simulates the effect of the POP instructions, popping from a passed "stack pointer" and returning the popped value 
static void * pop(void** stack){ 
    void* temp = *(void**)(*stack); 
    *stack += sizeof(void*); 
    return temp; 
} 

//Disables the write protection of an address, so we can override it 
static int unprotect(void * POINTER){ 
    const int PAGESIZE = sysconf(_SC_PAGE_SIZE);; 
    if (mprotect((void*)(((uintptr_t)POINTER & ~(PAGESIZE-1))), PAGESIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) { 
    fprintf(stderr, "Failed to set permission on %p\n", POINTER); 
    return 1; 
    } 
    return 0; 
} 

//Debug stuff 
static void fprintfhex(FILE* f, void * hash, int len) { 
    for (int i=0;i<len;i++) { 
    if ((uintptr_t)hash % 8 == 0 && (uintptr_t)i % 8 == 0 && i) fprintf(f, " "); 
    fprintf(f, "%.2x", ((unsigned char*)(hash))[i]); 
    } 
    fprintf(f, "\n"); 
} 

//--------------------------------------- 


//Address of the original readline function 
static char* (*real_readline)(const char*)=0; 

//The wrapper around readline we want to inject. 
//It replaces() with [], if the command line starts with "C " 
static char* readline_wrapper(const char* prompt){ 
    if (!real_readline) return 0; 
    char* result = real_readline(prompt); 
    char* temp = result; while (*temp == ' ') temp++; 
    if (temp[0] == 'C' && temp[1] == ' ') 
    for (int len = strlen(temp), i=0;i<len;i++) 
     if (temp[i] == '(') temp[i] = '['; 
     else if (temp[i] == ')') temp[i] = ']'; 
    return result; 
} 


//Backup of the changed readline part 
static unsigned char oldreadline[2*sizeof(LongJump)] = {0x90}; 
//A wrapper around the readline wrapper, needed on amd64 (see below) 
static LongJump* readline_wrapper_wrapper = 0; 



static void readline_initwrapper(){ 
    SAVE_PARAMETERS 
    if (readline_wrapper_wrapper) { fprintf(stderr, "ERROR!\n"); return; } 

    //restore readline 
    memcpy(real_readline, oldreadline, 2*sizeof(LongJump)); 

    //find call in yy_readline_get 
    void * frame; 
    __asm__(MOV_EBP_OUT: "=r"(frame)); //current stackframe 
    pop(&frame); //pop current stackframe (??) 
    void * returnToFrame = frame; 
    if (pop(&frame) != real_readline) { 
    //now points to current return address 
    fprintf(stderr, "Got %p instead of %p=readline, when searching caller\n", frame, real_readline); 
    return; 
    } 
    void * caller = pop(&frame); //now points to the instruction following the call to readline 
    caller -= RELATIVE_CALL_INSTRUCTION_SIZE; //now points to the call instruction 
    //fprintf(stderr, "CALLER: %p\n", caller); 
    //caller should point to 0x00000000004229e1 <+145>: e8 4a e3 06 00 call 0x490d30 <readline> 
    if (*(unsigned char*)caller != 0xE8) { fprintf(stderr, "Expected CALL, got: "); fprintfhex(stderr, caller, 16); return; } 

    if (unprotect(caller)) return; 

    //We can now override caller to call an arbitrary function instead of readline. 
    //However, the CALL instruction accepts only a 32 parameter, so the called function has to be in the same 32-bit address space 
    //Solution: Allocate memory at an address close to that CALL instruction and put a long jump to our real function there 
    void * hint = caller; 
    readline_wrapper_wrapper = 0; 
    do { 
    if (readline_wrapper_wrapper) munmap(readline_wrapper_wrapper, 2*sizeof(LongJump)); 
    readline_wrapper_wrapper = mmap(hint, 2*sizeof(LongJump), PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); 
    if (readline_wrapper_wrapper == MAP_FAILED) { fprintf(stderr, "mmap failed: %i\n", errno); return; } 
    hint += 0x100000; 
    } while (IS64BIT && ((uintptr_t)readline_wrapper_wrapper >= 0xFFFFFFFF + ((uintptr_t) caller))); //repeat until we get an address really close to caller 
    //fprintf(stderr, "X:%p\n", readline_wrapper_wrapper); 
    makeLongJump(readline_wrapper, readline_wrapper_wrapper); //Write the long jump in the newly allocated space 

    //fprintfhex(stderr, readline_wrapper_wrapper, 16); 
    //fprintfhex(stderr, caller, 16); 

    //patch caller to become call <readline_wrapper_wrapper> 
    //called address is relative to address of CALL instruction 
    *(uint32_t*)(caller+1) = (uint32_t) ((uintptr_t)readline_wrapper_wrapper - (uintptr_t)(caller + RELATIVE_CALL_INSTRUCTION_SIZE)); 

    //fprintfhex(stderr, caller, 16); 

    *(void**)(returnToFrame) = readline_wrapper_wrapper; //change stack to jump to wrapper instead real_readline (or it would not work on the first entered command) 

    RESTORE_PARAMETERS 
} 




static void _calc_init(void) __attribute__ ((constructor)); 


static void _calc_init(void){ 
    if (!real_readline) { 
    //Find readline 
    real_readline = (char* (*)(const char*)) dlsym(RTLD_DEFAULT, "readline"); 
    if (!real_readline) return; 
    //fprintf(stdout, "loaded %p\n", real_readline); 
    //fprintf(stdout, " => %x\n", * ((int*) real_readline)); 

    if (unprotect(real_readline)) { fprintf(stderr, "Failed to unprotect readline\n"); return; } 
    memcpy(oldreadline, real_readline, 2*sizeof(LongJump)); //backup readline's instructions 

    //Replace readline with readline_initwrapper 
    makeLongJump(real_readline, (LongJump*)real_readline); //add a push/ret long jump from readline to readline, to have readline's address on the stack in readline_initwrapper 
    makeLongJump(readline_initwrapper, (LongJump*)((char*)real_readline + sizeof(LongJump) - 1)); //add a push/ret long jump from readline to readline_initwrapper, overriding the previous RET 

    } 
} 

Questo può essere compilato in un intercettazione biblioteca con:

gcc -g -std=c99 -shared -fPIC -o calc.so -ldl calc.c 

e poi caricato in bash con:

gdb --batch-silent -ex "attach $BASHPID" -ex 'print dlopen("calc.so", 0x101)' 

Ora, quando l'alias precedente esteso con la sostituzione parentesi è caricato:

alias C='set -f -B; Cf ' 
function Cf() { echo "[email protected]" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | bc -l; set +f; }; 

possiamo scrivere:

$ C 1 * 2 
    2 
$ C 2*(2+1) 
    6 
$ C (2+1)*2 
    6 

Ancora meglio diventa, se si passa da BC a qalculate:

alias C='set -f -B; Cf ' 
function Cf() { echo "[email protected]" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | xargs qalc ; set +f; }; 

Quindi possiamo fare:

$ C e^(i * pi) 
    e^(i * pi) = -1 

$ C 3 c 
    3 * speed_of_light = approx. 899.37737(km/ms) 
+0

Freddo. Grazie per questo suggerimento! È un grande miglioramento rispetto alla mia soluzione iniziale. Apprezzo l'aiuto. – arielf

+0

Un'altra stranezza: questa modifica spezza altre espressioni: 'C 1 * 2' dà 0, 'C 1 + 2' dà 2. – arielf

+0

strano, ha funzionato bene per me. – BeniBela

3

Se siete pronti a digitare CInserire invece di CSpazio, il cielo è il limite. Il comando C può ricevere input in qualsiasi forma desideri, non correlato alla sintassi della shell.

C() { 
    local line 
    read -p "Arithmetic: " -e line 
    echo "$line" | tr -d \"-\', | bc -l 
} 

In zsh:

function C { 
    local line= 
    vared -p "Arithmetic: " line 
    echo $line | tr -d \"-\', | bc -l 
} 

In zsh, è possibile disattivare il globbing per gli argomenti di un comando specifico con la noglob modifier. È comunemente nascosto in un alias. Questo impedisce *^() di iniziare interpretato letteralmente, ma non virgolette o $.

quickie_arithmetic() { 
    echo "$*" | tr -d \"-\', | bc -l 
} 
alias C='noglob quickie_arithmetic' 
+0

Grazie! questa è una svolta abbastanza intelligente. È evidente a ben vedere, ma mi è mancato totalmente quando ho posto la domanda originale. – arielf

Problemi correlati